<?php 
/*
* PicoGeSHi
*/

class PicoGeSHi extends AbstractPicoPlugin
{
    const API_VERSION = 3;

    protected $enabled = null;

    // only act if this var is set to true
    private $yeah = FALSE;

    public function onConfigLoaded(array &$config)
    {
      $this->debug = FALSE;
      $this->geshi_class = 'geshi';
      $this->geshi_line_numbers = FALSE;
      $this->geshi_fancy_line_numbers = 0;
      $this->geshi_line_start = 0;
      $this->geshi_template = "post";
      $this->geshi_enable_ids = FALSE;
      $this->geshi_col_enabled = TRUE;
      // default CSS template - path is relative to plugin dir (where __FILE__ resides)
      $this->css_template = "default.template.css";
      // not (currently) configurable
      // parsedown prepends this to the language for code block classes, e.g.: <code class="language-php">...
      $this->langprefix = "language-";

      // colors used for stylesheet
      $this->colorname = array('fg_def','fg_alt','bg_def','bg_alt','red_norm','red_bold','green_norm','green_bold','yellow_norm','yellow_bold','blue_norm','blue_bold','magenta_norm','magenta_bold','cyan_norm','cyan_bold');

      if($config['debug'] === TRUE) {
        $this->debug = TRUE;
        ini_set('display_errors', 'On');
        error_reporting(E_ALL);
        echo "function ". __FUNCTION__ ." in ". __FILE__ .'<br/>';
      }

      // reading config values or setting fallback config values - importing them to this plugin's context
      if (isset($config['geshi']['class'])) $this->geshi_class = $config['geshi']['class'];

      if (isset($config['geshi']['line-numbers'])) $this->geshi_line_numbers = $config['geshi']['line-numbers'];

      if (isset($config['geshi']['fancy-line-numbers']) && $config['geshi']['fancy-line-numbers'] > 1) $this->geshi_fancy_line_numbers = $config['geshi']['fancy-line-numbers'];

      if (isset($config['geshi']['line-start']) && $config['geshi']['line-start'] >= 0) $this->geshi_line_start = $config['geshi']['line-start'];

      if (isset($config['geshi']['template']) && $config['geshi']['template'] != "") $this->geshi_template = $config['geshi']['template'];

      if (isset($config['geshi']['enable-ids'])) $this->geshi_enable_ids = $config['geshi']['enable-ids'];

      // check geshi colors defined in some config .yml
      if (isset($config['geshi_col']['enabled'])) $this->geshi_col_enabled = $config['geshi_col']['enabled'];
      //  - use fallback if desired (geshi_col.enabled is TRUE) but undefined
      if ($this->geshi_col_enabled == TRUE)
      {
          // fallback colors, with a light (dark text on light background) theme in mind
          // names from here: www.w3schools.com/cssref/css_colors.asp
          $fallback=array('Black','DimGrey','Mocassin','OldLace','DarkRed','FireBrick','DarkGreen','ForestGreen','DarkGoldenRod','DarkOrange','CadetBlue','CornflowerBlue','DarkViolet','DarkOrchid','DarkCyan','DeepSkyBlue');
          for($i=0;$i<16;$i++) {
            $color = $this->colorname[$i]; 
            if(isset($config['geshi_col'][$color])) $this->$color = $config['geshi_col'][$color];
            else $this->$color = $fallback[$i];
          }
      }

      // define default css twig template if not user-defined
      if (isset($config['geshi']['css_template'])) $this->css_template = $config['geshi']['css_template'];
    }

    public function onMetaParsed(array  $meta)
    {
      if($this->debug === TRUE) echo "function ". __FUNCTION__ ." found template \"". $meta['template'] ."\" in ". __FILE__ .'<br/>';
      // Only Do Things if we have the correct template
      if($meta['template'] === $this->geshi_template || $this->geshi_template === "all" ) $this->yeah = TRUE;
    }

    public function onContentParsed(&$content)
    {
      if($this->debug === TRUE) echo "function ". __FUNCTION__ ." in ". __FILE__ .'<br/>';
      if($this->yeah === FALSE ) {
        if($this->debug === TRUE) echo "We're not doing anything. Bye!".'<br/>';
        return;
      }
      if(trim($content)=="") {
        if($this->debug === TRUE) echo "Content is empty. Bye!".'<br/>';
        return;
      }
      $doc = new DOMDocument('1.0', 'utf-8');
      // enable user error handling
      // stackoverflow.com/questions/8218230/php-domdocument-loadhtml-not-encoding-utf-8-correctly
      libxml_use_internal_errors(true);
      if(!$doc->loadHTML($content))
      {
            foreach (libxml_get_errors() as $error) {
            $this->content['err'][] = $error;
        }
        libxml_clear_errors();
        return;
      }
      $doc->encoding="UTF-8"; // someone on the 'net wrote this should be done _after_ loadHTML
      $nodes = $doc->getElementsByTagName('code'); // it is possible to put a language class on a single line code element
      foreach($nodes as $node)
      {
        if($this->debug === TRUE) echo "We have at least one 'code' tag...";
        $lang = $node->getAttribute('class');
        // is there at least one with a language-class?
        $len = strlen($this->langprefix);
        if(substr($lang,0,$len) === $this->langprefix)
        // if yes, we start for real:
        {
          if($this->debug === TRUE) echo "...with a language:<br/>";
          //////////////////////////////////////
          require_once 'geshi-1.0/src/geshi.php';
          //////////////////////////////////////
          $geshi = new GeSHi('dummytext','php'); // so we need to open this only once for the whole page
          $geshi->set_encoding('UTF-8');
          $geshi->enable_strict_mode(TRUE);
          $geshi->enable_keyword_links(false);
          if($this->geshi_line_numbers === TRUE) $geshi->enable_line_numbers(GESHI_NORMAL_LINE_NUMBERS);
          if($this->geshi_fancy_line_numbers > 1) $geshi->enable_line_numbers(GESHI_FANCY_LINE_NUMBERS,$this->geshi_fancy_line_numbers);
          $geshi->start_line_numbers_at($this->geshi_line_start);
          if($this->geshi_enable_ids) {
            $geshi->enable_ids(TRUE);
            $geshi->set_overall_id($this->geshi_class);
          }
          $geshi->enable_classes();
          $geshi->set_header_type(GESHI_HEADER_PRE);

          foreach($nodes as $node)
          {
            $lang = $node->getAttribute('class');
            // no "language-" class? No highlighting desired.
            $len = strlen($this->langprefix);
            if(substr($lang,0,$len) !== $this->langprefix) continue;
    
            $str = $node->nodeValue;
            $glang = substr($lang,$len); // remove classprefix, usually "language-"
            $glang = preg_replace('/[^[:alnum:]]/','',$glang); // remove dots or curly brackets - everything but letters and digits
            switch($glang) {
              case 'sh': $glang = 'bash'; break;
              case 'patch': $glang = 'diff'; break;
            }
            if($this->debug === TRUE) echo "--- ".$lang." becomes ".$glang."<br/>";
            $geshi->set_source($str);
            $geshi->set_language($glang);
            $str = $geshi->parse_code();
    
            $code = $doc->createElement('code');
            $code->setAttribute("class",$this->geshi_class .' '.$glang.' '.$lang);
            $code->setAttribute("id",$this->geshi_class);
            $tmpDoc = new DOMDocument();
            $tmpDoc->loadHTML($str);
            $tmpDoc->encoding="UTF-8";
            foreach ($tmpDoc->getElementsByTagName('pre')->item(0)->childNodes as $tmpnode) {
                $tmpnode = $doc->importNode($tmpnode, true);
                $code->appendChild($tmpnode);
            }
            $node->parentNode->replaceChild($code,$node);
          }
          $content = $doc->saveXML($doc->documentElement);
          $len = strlen('<html><body>');
          if(substr($content,0,$len) === '<html><body>') $content = substr($content,$len);
          $len = '-'.strlen('</body></html>');
          if(substr($content,$len) === '</body></html>') $content = substr($content,0,$len);

          // applying CSS colors
          if($this->geshi_col_enabled === TRUE) {
            $styles = file_get_contents(dirname(__FILE__).'/'.$this->css_template);
            if($styles !== '')
            {
              $styles = str_replace('%class%',$this->geshi_class,$styles);
              for($i=0;$i<16;$i++) {
                $color = $this->colorname[$i];
                $styles = str_replace('%'.$color.'%',$this->$color,$styles);
              }
              //// MINIFY ON THE FLY
              // Remove comments
              $styles = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $styles);
              // Remove space after colons
              $styles = str_replace(': ', ':', $styles);
              // Remove newlines and tabs
              $styles = str_replace(array("\r\n", "\r", "\n", "\t"), '', $styles);
              // Collapse adjacent spaces into a single space
              $styles = preg_replace('!\s+!', ' ',$styles);
              // Remove spaces that might still be left where we know they aren't needed
              $srch = array('} ',  '{ ',  '; '  ,', '  ,' }'  ,' {'  ,' ;'  ,' ,' );
              $rplc = array('}' ,  '{' ,  ';'   ,','   ,'}'   ,'{'   ,';'   ,','  );
              $styles = str_replace($srch,$rplc,$styles);
              $content = '<style>'.$styles.'</style>'.$content;
            }
          }
        }
        else {
          if($this->debug === TRUE) echo "...but it doesn't have a language.<br/>";
          continue;
        }
        // this was not the real loop, we were only testing if we had at least one, can break now
        break;
      }
    }
}